import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

/*
 * Class Lexer
 * 
 * Update the method nextToken() such to the provided
 * specifications of the Decaf Programming Language.
 * 
 * You are not allowed to use any built it in tokenizer
 * in Java. You are only allowed to scan the input file
 * one character at a time.
 */

public class Lexer {

	private BufferedReader reader; // Reader
	private char curr; // The current character being scanned
	boolean pointReturn = false; // will be used if a "." is needed to be returned as
								 // a token as in 0. or .0 errors.
	
	public int lineNumber;
	public int charNumber;
	public String fileName;

	private static final char EOF = (char) (-1);

	// End of file character

	public Lexer(String file) {
		try {
			this.fileName = file;
			lineNumber =1;
			charNumber =0;
			
			reader = new BufferedReader(new FileReader(file));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		// Read the first character
		curr = read();
	}

	private char read() {
		try {
			return (char) (reader.read());
		} catch (IOException e) {
			e.printStackTrace();
			return EOF;
		}
	}

	/**
	 * Checks if the character is a digit.
	 * 
	 * @param c
	 * @return boolean true if c is a digit.
	 */
	private boolean isNumeric(char c) 
	{
		if (c >= '0' && c <= '9')
			return true;

		return false;
	}

	/**
	 * checks if a character is valid to be ID
	 * on parsing
	 * 
	 * @param c
	 * @return true if c is valid for ID.
	 */
	private boolean isValidforID(char c)
	{
		if(c >= 'a' && c <= 'z')
			return true;
		
		if(c >= 'A' && c <= 'Z')
			return true;
		
		if(c >= '0' && c <= '9')
			return true;
		
		if(c == '_')
			return true;
		
		return false;
	}
	

	/**
	 * takes a string and returns true if the string is equal "true" or "false"
	 * 
	 * @param s
	 * @return true if s is equal "true" or "false"
	 */
	
	private boolean isBoolean(String s)
	{
		if(s.equalsIgnoreCase("true")) return true;
		if(s.equalsIgnoreCase("false")) return true;
		
		return false;
	}
	
	/**
	 * Takes a string and returns true if the string is a pre-defined keyword in the
	 * language.
	 * 
	 * @param s
	 * @return true if s is a keyword in the language.
	 */
	private boolean isKeyword(String s)
	{
		if( s.equals("class"))     return true;
		if( s.equals("static"))    return true;
		if( s.equals("boolean"))   return true;
		if( s.equals("return"))    return true;
		if( s.equals("if"))        return true;
		if( s.equals("while"))     return true;
		if( s.equals("int"))       return true;
		if( s.equals("else"))      return true;
		if( s.equals("float"))     return true;
		if( s.equals("String"))    return true;
		
		return false;
	}
	
	
	
	private Token tokenSeparator(String s)
	{
		if(s.equals("==")) return new Token(Token.EQ, "==");
		if(s.equals("!=")) return new Token(Token.NE, "!=");
		
		return null;
	}
	
	/**
	 * takes an a natural number and removes the zeros in the start of the number.
	 * 
	 * @param number
	 * @return a string with the number without the starting zeros
	 */
	private String removeZeros(String number)
	{
		while(true)
		{
			if(number.length()==0)
				break;
			
			if(number.charAt(0)=='0')
			{
				if(number.length()>1)
				{
					number = number.substring(1);
				}
				else
					break;
			}
			else
				break;
		}
		return number;
	}
	
	/**
	 * takes two numbers (a decimal point is considered to be between them), then removes the
	 * zeros in the numbers.
	 * @param number1 : number before the decimal point
	 * @param number2 : number after the decimal point
	 * @return a token contains the updated number.
	 */
	private Token numberToken(String number1, String number2)
	{
		number1= removeZeros(number1);
		String number2Temp;
		number2Temp = new StringBuffer(number2).reverse().toString();
		number2Temp= removeZeros(number2);
		number2Temp = new StringBuffer(number2Temp).reverse().toString();
		
		if(number2.length()>0)
		{
			if (number2Temp.length()==0)
			{
				number2Temp = "0";
			}
		}
		
		if(number1.equals(""))
		{
			number1 = "0";
		}
		if(number2Temp.equals(""))
		{
			return new Token(Token.NM,number1);
		}
		else
		{
			return new Token(Token.NM,number1+"."+number2Temp);
		}
	}

	public Token nextToken() {

		int state = 1; // Initial state
		String numBuffer1 = ""; // A buffer for number literals
		String numBuffer2 = "";
		String stringBuffer = "";
		

		while (true) 
		{
			
			if(pointReturn)
			{
				pointReturn = false;
				String W = "Warning at line " + lineNumber +" char "; 
				W+= (charNumber-1) + "\n";
				W+= "a '.' is unnessasry added \n";
				W+= "In  "+getLine(lineNumber );
				W+= "\n-------------------------------------------- \n";
				System.out.println(W);
				charNumber++;
				state = 1;
//				return new Token(Token.ERR, "Invalid input: .");
			}
			
			if (curr == EOF) 
			{
//				try 
//				{
					return new Token(Token.EOF,"");
//				} 
//				catch (IOException e)
//				{
//					e.printStackTrace();
//				}
//				return null;
			}

			switch (state) 
			{
			// Start Case 
			case 1:
				switch (curr) 
				{
				case  ' ': 
				case '\t':
				case '\b':
				case '\f':
					charNumber ++;
					curr = read();
					continue;
				case '\r':
				case '\n':
					lineNumber++;
					charNumber =0;
					curr = read();
					continue;

				case ';':
					charNumber ++;
					curr = read();
					return new Token(Token.SM, ";");

				case '+':
					charNumber ++;
					curr = read();
					return new Token(Token.PO, "+");

				case '-':
					charNumber ++;
					curr = read();
					return new Token(Token.MO, "-");

				case '*':
					charNumber ++;
					curr = read();
					return new Token(Token.TO, "*");
					
				case '%':
					charNumber ++;
					curr = read();
					return new Token(Token.MD, "%");
				
				case '{':
					charNumber ++;
					curr = read();
					return new Token(Token.LB,"{");
					
				case '}':
					charNumber ++;
					curr = read();
					return new Token(Token.RB,"}");
				
				case '(':
					charNumber ++;
					curr = read();
					return new Token(Token.LP,"(");

				case ')':
					charNumber ++;
					curr = read();
					return new Token(Token.RP,")");
				
				case ',':
					charNumber ++;
					curr = read();
					return new Token(Token.FA,",");

					
				// checking for commenting, we go to state 13.
				case '/':
					charNumber ++;
					state = 13;
					curr = read();
					if(curr == EOF)
					{
						return new Token(Token.DO, stringBuffer);
					}
					continue;
				
				// checking for "==", we go to state 5.
				case '=':
					charNumber ++;
					stringBuffer = stringBuffer+ curr;
					curr = read();
					if(curr == EOF)
					{
						return new Token(Token.AO, stringBuffer);
					}
					state= 5;
					continue;
				
				// checking for "!=", we go to state 5.
				case '!':
					charNumber ++;
					stringBuffer = stringBuffer+ curr;
					curr = read();
					if(curr == EOF)
					{
						state = 1;
						String W = "Warning at line " + (lineNumber-1) +" char "; 
						W+= (charNumber-1) + "\n";
						W+= "The character ! is invalid \n";
						W+= "In  "+getLine(lineNumber );
						W+= "\n-------------------------------------------- \n";
						System.out.println(W);
						continue;
						//return new Token(Token.ERR, "Invalid input: "+stringBuffer);
					}
					state= 5;
					continue;
				
				// checking for "||", we go to state 7.
				case '|':
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					if(curr == EOF)
					{
						state = 1;
						String W = "Warning at line " + (lineNumber-1) +" char "; 
						W+= (charNumber-1) + "\n";
						W+= "The character | is invalid \n";
						W+= "In  "+getLine(lineNumber );
						W+= "\n-------------------------------------------- \n";
						System.out.println(W);
						continue;
//						return new Token(Token.ERR, "Invalid input: "+stringBuffer);
					}
					state = 7;
					continue;
				
				// checking for "&&", we go to state 9.
				case '&':
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					if(curr == EOF)
					{
						state = 1;
						String W = "Warning at line " + (lineNumber-1) +" char "; 
						W+= (charNumber-1) + "\n";
						W+= "The character & is invalid \n";
						W+= "In  "+getLine(lineNumber );
						W+= "\n-------------------------------------------- \n";
						System.out.println(W);
						continue;
//						return new Token(Token.ERR, "Invalid input: "+stringBuffer);
					}
					state = 9;
					continue;
				
				// checking if the lateral is a string, we go to state 11.
				case '"':
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					state = 11;
					continue;
	
				// at least we check if it is a numeric string ( a number ) in state 2.	
				default:
					state = 2; // Check the next possibility
					continue;
				}

				
			case 2:
				//if is number
				if (isNumeric(curr)) 
				{
					charNumber ++;
					numBuffer1 = ""; // Reset the buffer.
					numBuffer1 += curr;

					state = 3;
					curr = read();
					if(curr == EOF)
					{
						return numberToken(numBuffer1,"");
					}
				} 
				else
				{
					state = 12;
					
				}
				continue;

			case 3:
				//continue if we have a number 
				
				if (isNumeric(curr) || curr == '.') 
				{ 
					if(curr == '.')
					{
						charNumber ++;
						state = 4;
						curr= read();
						if(curr == EOF)
						{
							pointReturn = true;
							return numberToken(numBuffer1,"");
						}
						else
						{
							if(!isNumeric(curr))
							{
								pointReturn = true;
								return numberToken(numBuffer1,"");
							}
							else
							{
								state = 4;
								continue;
							}
						}
						
					}
					charNumber ++;
					numBuffer1 += curr;
					curr = read();	
					if(curr == EOF)
					{
						return numberToken(numBuffer1,"");
					}
					
				} 
				else 
				{
					return numberToken(numBuffer1,"");
				}
				continue;
				
			//for decimal numbers, we accumulate the numbers after the
			// decimal points in this state
			case 4:
				if(isNumeric(curr))
				{
					charNumber ++;
					numBuffer2+=curr;
					curr = read();
					if(curr == EOF)
					{
						return numberToken(numBuffer1,numBuffer2);
					}
					continue;
					
				}
				else
				{
					return numberToken(numBuffer1,numBuffer2);
				}
				
			
			case 5:
				// continue != or ==
				if(curr == '=')
				{
					charNumber ++;
					stringBuffer += curr;
					curr = read();
					return tokenSeparator(stringBuffer);
				}
				else
				{
					if(stringBuffer.equals("="))
					{
						return new Token(Token.AO , "=");
					}
					else
					{
						charNumber++;
						state = 1;
						stringBuffer = "";
						String W = "Warning at line " + lineNumber +" char "; 
						W+= (charNumber-1) + "\n";
						W+= "character ! is invalid \n";
						W+= "In  "+getLine(lineNumber );
						W+= "\n-------------------------------------------- \n";
						System.out.println(W);
						continue;
					}
				}
				
				
			
			case 7:
				// for || checking
				if(curr == '|')
				{
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					return new Token(Token.LO , stringBuffer);
				}
				else
				{
					charNumber++;
					curr= read();
					String W = "Warning at line " + lineNumber +" char "; 
					W+= (charNumber-1) + "\n";
					W+= "| converted to || \n";
					W+= "In  "+getLine(lineNumber );
					W+= "\n-------------------------------------------- \n";
					System.out.println(W);
					return new Token(Token.LO,"||");
				}
				
			// for && checking
			case 9:
				
				if(curr == '&')
				{
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					return new Token(Token.LA , stringBuffer);
				}
				else
				{
					charNumber ++;
					curr= read();
					String W = "Warning at line " + lineNumber +" char "; 
					W+= (charNumber-1) + "\n";
					W+= "& converted to && \n";
					W+= "In  "+getLine(lineNumber );
					W+= "\n-------------------------------------------- \n";
					System.out.println(W);
					return new Token(Token.LA,"&&");
				}
				
			// to check strings
			case 11:
				if(curr =='"')
				{
					charNumber ++;
					stringBuffer+= curr;
					curr = read();
					return new Token(Token.ST, stringBuffer);
				}
				else
				{
					if(curr == '\n' || curr == '\r')
					{
						String W = "Warning at line " + (lineNumber-1) +" char "; 
						W+= (charNumber) + "\n";
						W+= "A String lateral was not closed\n";
						W+= "In  "+getLine(lineNumber );
						W+= "\n-------------------------------------------- \n";
						System.out.println(W);
						lineNumber++;
						charNumber=0;
						curr = read();
						stringBuffer+="\"";
						return new Token(Token.ST, stringBuffer);
					}
					else
					{
						charNumber ++;
						stringBuffer+= curr;
						curr = read();
						
						if(curr == EOF)
						{
							String W = "Warning at line " + (lineNumber-1) +" char "; 
							W+= (charNumber) + "\n";
							W+= "A String lateral was not closed\n";
							W+= "In  "+getLine(lineNumber );
							W+= "\n-------------------------------------------- \n";
							System.out.println(W);
							return new Token(Token.ST, stringBuffer+"\"");
						}
						
						continue;
					}
				}
			
			case 12:
				
				// if an ID or a keyword.	
				if(!isValidforID(curr))
				{
					char temp = curr;
					curr = read();
					charNumber++;
					state = 1;
					String W = "Warning at line " + (lineNumber-1) +" char "; 
					W+= (charNumber-1) + "\n";
					W+= "The character "+ temp+ " is invalid \n";
					W+= "In  "+getLine(lineNumber );
					W+= "\n-------------------------------------------- \n";
					System.out.println(W);
					continue;
//					return new Token(Token.ERR,"invalid input: "+temp);
				}
				else
				{
					state = 17;
					continue;
				}
			
			case 17:
				
				if(!isValidforID(curr))
				{
					
					if(isKeyword(stringBuffer))
					{
						return new Token(Token.KW, stringBuffer);
					}
				
					else
					{
						if(isBoolean(stringBuffer))
						{
							return new Token(Token.BL, stringBuffer);
						}
						else
						{
							return new Token(Token.ID, stringBuffer);
						}
					}
					
					
				}
				else 
				{
					charNumber ++;
					stringBuffer= stringBuffer +curr;
					curr= read();
					if(curr == EOF)
					{
						if(isKeyword(stringBuffer))
						{
							return new Token(Token.KW, stringBuffer);
						}
					
						else
						{
							if(isBoolean(stringBuffer))
							{
								return new Token(Token.BL, stringBuffer);
							}
							else
							{
								return new Token(Token.ID, stringBuffer);
							}
						}
					}
					continue;
				}
				
			case 13:
				
				// commenting checking (line comment)
				if(curr == '/')
				{
					charNumber ++;
					state = 14;
					curr = read();
					continue;
				}
				else
				{
					// commenting checking (block comment)
					if(curr == '*')
					{
						charNumber ++;
						state = 15;
						curr = read();
						continue;
					}
					else
					{
						return new Token(Token.DO,"/");
					}
				}
				
			
			//cases 14,15,16 are for comment blocks
			case 14:
				
				if(curr == '\n')
				{
					state = 1;
					lineNumber++;
					charNumber=0;
				}	
					curr = read();
				continue;
				
				
			case 15:
				charNumber ++;
				if(curr == '*')
					state = 16;
				if(curr == '\n' || curr == '\r')
				{
					lineNumber++;
					charNumber=0;
				}
				
				curr = read();
				continue;
				
				
			case 16:
				
				if(curr == '/')
				{
					state = 1;
				}
				else
				{
					state = 15;
				}
				charNumber ++;
				if(curr == '\n' || curr == '\r')
				{
					lineNumber++;
					charNumber=0;
				}
				curr = read();
				continue;
					
			
			}
		}
	}
	
	public String getLine(int number)
	{
		try
		{
			BufferedReader r = new BufferedReader(new FileReader(fileName));
			for(int i = 1 ; i< number ; i++)
			{
				r.readLine();
			}
			return r.readLine();
		}
		catch( Exception e )
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "NOTHING";
	}
	



}
